-- -------------------------------------------------------------------------
-- UI/notification.lua
-- Notification System
-- -------------------------------------------------------------------------

local notification = {}

local state = require("ClickUl.UI.state")
local renderer = require("ClickUl.UI.renderer")

-- [Lua 5.4] <const> for function references
local math_floor <const> = math.floor
local math_max <const> = math.max
local math_min <const> = math.min
local math_abs <const> = math.abs
local math_exp <const> = math.exp
local os_clock <const> = os.clock
local table_insert <const> = table.insert
local table_remove <const> = table.remove
local ipairs <const> = ipairs
local string_find <const> = string.find
local tostring <const> = tostring

-- [Icon Cache with LRU]
local icon_cache = {}
local icon_cache_order = {}
local icon_order_index = {}
local ICON_CACHE_LIMIT <const> = 50

-- [Failed Icon Cache]
local failed_icon_cache = {}

-- [Translation Helper]
local function _T(text)
    if _G and _G._T then
        return _G._T(text)
    end
    return text
end

-- [Static Reusable Objects]
local STATIC_WHITE <const> = {r=255, g=255, b=255, a=255}

-- [Reusable Color Tables]
local TMP_FLASH_COLOR = {r=255, g=255, b=255, a=0}
local TMP_TRACK_COLOR = {r=0, g=0, b=0, a=0}
local TMP_BAR_COLOR = {r=0, g=0, b=0, a=255}

-- [Time Bar Gradient LUT]
local TIME_BAR_GRADIENT_LUT <const> = {}
for i = 0, 100 do
    local t = i / 100
    TIME_BAR_GRADIENT_LUT[i] = {
        r = math_floor(255 + (220 - 255) * (1 - t)),
        g = math_floor(255 + (53 - 255) * (1 - t)),
        b = math_floor(255 + (69 - 255) * (1 - t)),
        a = 255
    }
end

-- Reusable options table for gui.text_size
local TEXT_SIZE_OPTS <const> = { wrap = 0 }

-- [Cached Renderer References]
local s_func <const> = renderer.s
local get_vec2 <const> = renderer.get_vec2
local get_color_alpha <const> = renderer.get_color_alpha
local get_color <const> = renderer.get_color
local draw_rect_ex <const> = renderer.draw_rect_ex
local draw_text_ex <const> = renderer.draw_text_ex

-- [Animation Curve] Back Ease-Out 
local function ease_out_back(x)
    local c1 = 1.70158
    local c3 = c1 + 1
    return 1 + c3 * ((x - 1) ^ 3) + c1 * ((x - 1) ^ 2)
end

-- [Animation Curve] Cubic Ease-In
local function ease_in_cubic(x)
    return x * x * x
end

-- [Notification API]

function notification.notify(msg, type_str)
    local config_sys = state.deps.config_sys
    local ui = state.ui
    if not (config_sys and config_sys.data and config_sys.data.base) then return end
    local base = config_sys.data.base

    type_str = type_str or "Info"
    local color_key = "Notification Background"

    if type_str == "Info" then color_key = "Notification Background"
    elseif type_str == "Success" then color_key = "Notification Success"
    elseif type_str == "Warning" then color_key = "Notification Warning"
    elseif type_str == "Error" then color_key = "Notification Error"
    elseif type_str == "Join" then color_key = "Notification Join"
    elseif type_str == "Leave" then color_key = "Notification Leave"
    elseif type_str == "Modder Detection" then
        color_key = (config_sys.data.colors["Notification Modder"]) and "Notification Modder" or "Notification Error"
    elseif type_str == "Spectator" then color_key = "Notification Spectator"
    elseif type_str == "Script Event" then color_key = "Notification Script Event"
    elseif type_str == "Network Event" then color_key = "Notification Network Event"
    elseif type_str == "Protection" then color_key = "Notification Protection"
    elseif type_str == "Player Manager" then color_key = "Notification Player Manager"
    elseif config_sys.data.colors["Notification " .. type_str] then
        color_key = "Notification " .. type_str
    end

    local icon_tex = nil
    if gui and gui.load_image then
        local icon_filename = "logo.png"
        local icons = config_sys.data.notification_icons

        if icons then
            -- Map special cases (e.g., "Modder Detection" -> "Modder")
            local icon_key = type_str
            if type_str == "Modder Detection" then
                icon_key = "Modder"
            end

            -- Look up icon by key
            if icons[icon_key] then
                icon_filename = icons[icon_key]
            end
        end

        if icon_filename and icon_filename ~= "None" and icon_filename ~= "" then
            if icon_cache[icon_filename] then
                icon_tex = icon_cache[icon_filename]
                local idx = icon_order_index[icon_filename]
                if idx then
                    table_remove(icon_cache_order, idx)
                    -- Update indices for shifted entries
                    for j = idx, #icon_cache_order do
                        icon_order_index[icon_cache_order[j]] = j
                    end
                end
                icon_cache_order[#icon_cache_order + 1] = icon_filename
                icon_order_index[icon_filename] = #icon_cache_order
            elseif not failed_icon_cache[icon_filename] then
                local path = renderer.get_assets_dir() .. "Images\\" .. icon_filename
                local status, tex = pcall(gui.load_image, path)
                if status and tex then
                    -- LRU: Evict oldest if at limit
                    if #icon_cache_order >= ICON_CACHE_LIMIT then
                        local oldest = table_remove(icon_cache_order, 1)
                        if oldest then
                            icon_cache[oldest] = nil
                            icon_order_index[oldest] = nil
                            -- Update indices for shifted entries
                            for j = 1, #icon_cache_order do
                                icon_order_index[icon_cache_order[j]] = j
                            end
                        end
                    end
                    icon_tex = tex
                    icon_cache[icon_filename] = tex
                    icon_cache_order[#icon_cache_order + 1] = icon_filename
                    icon_order_index[icon_filename] = #icon_cache_order
                else
                    failed_icon_cache[icon_filename] = true
                end
            end
        end
    end

    local notif_w = s_func(base.notif_width or 320)
    local padding = s_func(base.notif_padding or 6)
    local icon_size = s_func(base.notif_icon_size or 32)
    local min_h_no_icon = s_func(base.notif_min_height or 60)
    local duration = base.notif_duration or 5.0
    local flash_cooldown = base.notif_flash_cooldown or state.constants.NOTIFICATION_FLASH_COOLDOWN or 1.5

    local f_title = base.notif_title_size or 17
    local f_msg = base.notif_message_size or 14

    local text_start_x = padding
    if icon_tex then text_start_x = padding + icon_size + padding end
    local max_text_w = notif_w - text_start_x - padding

    local title_h = s_func(f_title) + s_func(2)
    local msg_h = s_func(f_msg)

    if gui and gui.text_size then
        TEXT_SIZE_OPTS.wrap = max_text_w
        local dims = gui.text_size(msg, s_func(f_msg), TEXT_SIZE_OPTS)
        if dims and dims.y and dims.y > 0 then msg_h = dims.y end
    end

    if msg and msg ~= "" and msg_h < s_func(f_msg) then msg_h = s_func(f_msg) end

    local min_h = icon_tex and (icon_size + padding * 2) or min_h_no_icon
    local content_h = padding + title_h + msg_h + padding

    -- Add space for TimeBar at bottom if enabled
    if base.notif_time_bar_enabled ~= false then
        content_h = content_h + s_func(base.notif_time_bar_height or 2) + s_func(2)
    end

    local final_h = math_max(min_h, content_h)

    for _, notif in ipairs(ui.notifications) do
        if notif.text == msg and notif.title == type_str and notif.state ~= "out" then
            local now = os_clock()
            notif.start_time = now
            notif.count = (notif.count or 1) + 1
            notif.color_key = color_key
            notif.icon = icon_tex

            if notif.state == "out" then
                notif.state = "stay"
                notif.progress = 1.0
            end

            local time_since_last_flash = now - (notif.last_flash_time or 0)
            if time_since_last_flash >= flash_cooldown then
                notif.flash_intensity = 0.8
                notif.last_flash_time = now
            end
            return
        end
    end

    local max_count = base.notif_max_count or 12
    while #ui.notifications >= max_count do table_remove(ui.notifications, 1) end

    table_insert(ui.notifications, {
        title = type_str,
        text = msg,
        color_key = color_key,
        icon = icon_tex,
        start_time = os_clock(),
        duration = duration,
        state = "in",
        progress = 0.0,
        current_y = nil,
        count = 1,
        flash_intensity = 0.0,
        last_flash_time = os_clock(),
        height = final_h,
        width = notif_w,
        padding = padding,
        icon_size = icon_size,
        text_start_x = text_start_x,
        f_title = f_title,
        f_msg = f_msg,
        max_text_w = max_text_w
    })
end

-- Compatibility alias (used by debugger/init in ClickUl_Loader.lua)
notification.show = notification.notify

function notification.draw()
    local ui = state.ui
    if #ui.notifications == 0 then return end

    local config_sys = state.deps.config_sys
    if not (config_sys and config_sys.data and config_sys.data.base) then return end
    
    if not ui.open then renderer.update_scale() end 

    local now = os_clock()
    local dt = ui.delta_time or 0.016
    local screen_w = ui.screen_res.x
    local screen_h = ui.screen_res.y

    local base = config_sys.data.base
    local margin_x = s_func(base.notif_margin_x or 20)
    local margin_y = s_func(base.notif_margin_y or 20)
    local stack_spacing = s_func(base.notif_spacing or 8)
    local radius = s_func(base.notif_radius or 8)

    local anim_speed_in = base.notif_anim_speed_in or 4.0
    local anim_stack_speed = base.notif_anim_stack_speed or 15.0
    local default_padding = s_func(base.notif_padding or 6)

    local position = state.deps.config_sys.data.notification_position or "bottom_right"
    local is_top = string_find(position, "top") ~= nil
    local is_left = string_find(position, "left") ~= nil

    local k = 1
    while k <= #ui.notifications do
        local notif = ui.notifications[k]
        local elapsed = now - notif.start_time

        if (notif.state == "in" or notif.state == "stay") and elapsed > notif.duration then
            notif.state = "out"
        end

        local target = (notif.state == "out") and 0.0 or 1.0
        local diff = target - notif.progress

        local exit_speed_mult = base.notif_exit_speed_multiplier or 1.5
        local speed = (notif.state == "out") and (anim_speed_in * exit_speed_mult) or anim_speed_in
        notif.progress = notif.progress + (diff * (1.0 - math_exp(-speed * dt)))

        if math_abs(target - notif.progress) < 0.005 then notif.progress = target end

        local flash_decay_speed = base.notif_flash_decay_speed or 6.0
        if notif.flash_intensity and notif.flash_intensity > 0.01 then
            notif.flash_intensity = notif.flash_intensity * math_exp(-flash_decay_speed * dt)
        else
            notif.flash_intensity = 0
        end

        if notif.state == "out" and notif.progress <= 0.01 then
            table_remove(ui.notifications, k)
        else
            k = k + 1
        end
    end

    local current_stack_y = is_top and margin_y or (screen_h - margin_y)
    
    for i = #ui.notifications, 1, -1 do
        local notif = ui.notifications[i]
        
        if notif.progress > 0.01 then
            local pad = notif.padding or default_padding
            
            local target_y
            if is_top then
                target_y = current_stack_y
                current_stack_y = current_stack_y + notif.height + stack_spacing
            else
                target_y = current_stack_y - notif.height
                current_stack_y = current_stack_y - notif.height - stack_spacing
            end

            if not notif.current_y then notif.current_y = target_y end
            local y_diff = target_y - notif.current_y
            if math_abs(y_diff) > 0.5 then
                notif.current_y = notif.current_y + (y_diff * (1.0 - math_exp(-anim_stack_speed * dt)))
            else
                notif.current_y = target_y
            end

            local full_slide_dist = notif.width + margin_x + s_func(20)

            local slide_offset = 0
            if notif.state == "out" then
                -- Exit: Cubic acceleration (like falling)
                local t = 1.0 - notif.progress
                slide_offset = full_slide_dist * ease_in_cubic(t)
            else
                -- Enter: Elastic snap
                slide_offset = full_slide_dist * (1.0 - ease_out_back(notif.progress))
            end

            local x_pos = is_left and (margin_x - slide_offset) or (screen_w - margin_x - notif.width + slide_offset)

            local rx, ry = math_floor(x_pos), math_floor(notif.current_y)

            local content_alpha = notif.progress
            if notif.state == "out" then
                content_alpha = math_min(1.0, notif.progress * 2.0)
            end
            local text_alpha = content_alpha * content_alpha

            -- 1. Background (Linear Fade)
            draw_rect_ex(rx, ry, notif.width, notif.height,
                notif.color_key, radius, gui.rounding.all, notif.progress)

            -- 2. Flash
            if notif.flash_intensity and notif.flash_intensity > 0.01 then
                local flash_color = get_color("Notification Flash") or STATIC_WHITE
                local f_alpha = math_floor(notif.flash_intensity * (flash_color.a or 150) * content_alpha)
                if f_alpha > 0 then
                    TMP_FLASH_COLOR.r = flash_color.r or 255
                    TMP_FLASH_COLOR.g = flash_color.g or 255
                    TMP_FLASH_COLOR.b = flash_color.b or 255
                    TMP_FLASH_COLOR.a = f_alpha
                    draw_rect_ex(rx, ry, notif.width, notif.height, nil, radius, nil, nil, TMP_FLASH_COLOR)
                end
            end

            -- 3. Icon
            if notif.icon and gui.image then
                local iy = ry + (notif.height - notif.icon_size) / 2
                -- Adjust icon Y if timebar is present to center visually
                if base.notif_time_bar_enabled ~= false then
                    iy = iy - s_func(2)
                end
                local tint = get_color_alpha(STATIC_WHITE, content_alpha)
                gui.image(notif.icon, get_vec2(rx + pad, iy), get_vec2(notif.icon_size, notif.icon_size), tint)
            end

            -- 4. Text
            local tx, ty = rx + notif.text_start_x, ry + pad

            -- Title
            draw_text_ex(_T(notif.title), tx, ty, notif.f_title, nil, nil, nil, text_alpha, STATIC_WHITE)

            -- Message (Wrapped)
            local my = ty + s_func(notif.f_title) + s_func(1)

            local text_rendered = false
            if gui.text then
                local t = gui.text(notif.text)
                if t then
                    t:position(get_vec2(tx, my))
                    t:scale(math_floor(s_func(notif.f_msg)))
                    t:wrap(notif.max_text_w)
                    t:color(get_color_alpha(STATIC_WHITE, text_alpha))
                    t:draw()
                    text_rendered = true
                end
            end

            if not text_rendered then
                draw_text_ex(notif.text, tx, my, notif.f_msg, nil, nil, nil, text_alpha, STATIC_WHITE)
            end

            -- 5. Counter
            if notif.count > 1 then
                local c_str = "x" .. tostring(notif.count)
                local cw = renderer.get_text_width(c_str, notif.f_msg)
                draw_text_ex(c_str, rx + notif.width - cw - pad, ry + pad,
                    notif.f_msg, nil, nil, nil, text_alpha, STATIC_WHITE)
            end

            -- 6. Time Progress Bar
            if base.notif_time_bar_enabled ~= false and notif.state ~= "out" and notif.duration and notif.duration > 0 then
                local remaining_pct = 1.0 - ((now - notif.start_time) / notif.duration)
                if remaining_pct > 0 then
                    local bar_h = s_func(base.notif_time_bar_height or 2)
                    local bar_margin = s_func(4)
                    local max_bar_w = notif.width - (bar_margin * 2)
                    local bar_w = max_bar_w * remaining_pct
                    local bar_x = rx + bar_margin
                    local bar_y = ry + notif.height - bar_h - s_func(3)

                    local lut_idx = math_floor(remaining_pct * 100)
                    if lut_idx > 100 then lut_idx = 100 elseif lut_idx < 0 then lut_idx = 0 end
                    local bar_color_src = TIME_BAR_GRADIENT_LUT[lut_idx]
                    local bar_opacity = base.notif_time_bar_opacity or 180
                    TMP_BAR_COLOR.r = bar_color_src.r
                    TMP_BAR_COLOR.g = bar_color_src.g
                    TMP_BAR_COLOR.b = bar_color_src.b
                    TMP_BAR_COLOR.a = math_floor(bar_opacity * content_alpha)

                    -- Draw Track (Background for bar)
                    TMP_TRACK_COLOR.a = math_floor(40 * content_alpha)
                    draw_rect_ex(bar_x, bar_y, max_bar_w, bar_h, nil, s_func(1), nil, nil, TMP_TRACK_COLOR)

                    -- Draw Bar (solid color)
                    draw_rect_ex(bar_x, bar_y, bar_w, bar_h, nil, s_func(1), nil, nil, TMP_BAR_COLOR)
                end
            end

        else
            local eff_h = notif.height + stack_spacing
            if is_top then current_stack_y = current_stack_y + eff_h else current_stack_y = current_stack_y - eff_h end
        end
    end
end

function notification.clear_cache()
    icon_cache = {}
    icon_cache_order = {}
    icon_order_index = {}
    failed_icon_cache = {}
end

return notification
